Ce travail a été effectué par :
Le but du projet est d'utiliser une synthèse de données textuelles d'un grand nombre d'articles scientifiques sur le traitement du langage naturel. Le jeu de données TALN n'est pas vraiment grand, juste 44Mo, mais il est assez grand pour être trop compliqué pour être traité à la main. La manière de générer la synthèse et le type de synthèse sont totalement ouverts ! Nous pouvons nous concentrer sur les résumés ou les titres (moins d'effort de calcul) ou sélectionner un sous-ensemble spécifique. Si une approche ne fonctionne pas comme prévu, vous pouvez le montrer et l'expliquer. Ce projet contiendra un rapport pour expliquer la logique de nos approches et de nos résultats.
Dataset (44Mo) : https://www.ortolang.fr/market/corpora/corpus-taln
from google.colab import drive
drive.mount('/content/drive')
root = '/content/drive/MyDrive/'
root += 'A5/Advanced Machine Learning for Big Data and Text Processing/project1'
%%capture
%matplotlib inline
# system
import os
# regular expressions
import re
# xml trees (our dataset)
import xml.etree.ElementTree as ET
# data encoding
import unicodedata as ucd
# avoid warnings
import warnings
warnings.filterwarnings("ignore")
# deal with data
import numpy as np
import pandas as pd
pd.set_option('max_colwidth', 50) # default = 50
# display data
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [11, 4]
plt.rcParams['figure.dpi'] = 150
plt.rcParams['font.size'] = 7.5 # default = 10.0
# easy access
plt.default_colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
# sklearn for vectorization on tfidf and K-Means (unsupervised clusterization)
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
# beautiful visualization of clusters
from wordcloud import WordCloud
# Natural Langage Processing
import nltk
nltk.download('stopwords')
from nltk.tokenize import RegexpTokenizer
from nltk.corpus import stopwords
# to use the lemmatization package and its french and english vocabulary
import spacy
!pip install spacy
!python3 -m spacy download fr
!python3 -m spacy download en
# to perform and visualize LDA
! pip install pyldavis
import gensim
import pyLDAvis.gensim
pyLDAvis.enable_notebook()
# create a saving directory
save_dir = os.path.join(root, 'save_dir')
if not os.path.exists(save_dir):
os.makedirs(save_dir)
# to export the notebook
!apt-get install texlive texlive-xetex texlive-latex-extra pandoc
!pip install pypandoc
corpus_filepath = os.path.join(root, 'corpus-taln/2/corpus_taln_v1.tei.xml')
corpus_tree = ET.parse(corpus_filepath)
corpus_root = corpus_tree.getroot()
corpus_root.findall('./')[:7]
On voit qu'il existe des namespaces qui ne rendent pas la lecture et l'écriture facile alors nous allons nous créer un dictionnaire de ces namespaces pour le confort de tout le monde
ns = {
"tal": "http://www.tei-c.org/ns/1.0",
"xml": "http://www.w3.org/XML/1998/namespace"
}
Première vérification qui va déterminer la suite de notre étude. On regarde le nombre de publication, celles dont la langue est renseignée, celles en français et celles en anglais
print(
'Nombre de publication :',
len(corpus_root.findall('./tal:TEI', namespaces=ns))
)
print(
'Nombre de publication avec la langue renseignée :',
len(corpus_root.findall('./tal:TEI[@xml:lang]', ns))
)
print(
'Nombre de publication en francais :',
len(corpus_root.findall('./tal:TEI[@xml:lang="fr"]', ns))
)
print(
'Nombre de publication en anglais :',
len(corpus_root.findall('./tal:TEI[@xml:lang="en"]', ns))
)
On peut alors se dire que l'on va faire notre étude de texte sur les publications en français si on étudie les publications dans leur entièreté.
print(
'Nombre de publication avec abstract en francais :',
len(corpus_root.findall('./tal:TEI/tal:text/tal:front/tal:div[@type="abstract"][@xml:lang="fr"]', namespaces=ns))
)
print(
'Nombre de publication avec abstract en anglais :',
len(corpus_root.findall('./tal:TEI/tal:text/tal:front/tal:div[@type="abstract"][@xml:lang="en"]', namespaces=ns))
)
print(
'Nombre de publication avec keywords en francais :',
len(corpus_root.findall('./tal:TEI/tal:text/tal:front/tal:div[@type="keywords"][@xml:lang="fr"]', namespaces=ns))
)
print(
'Nombre de publication avec keywords en anglais :',
len(corpus_root.findall('./tal:TEI/tal:text/tal:front/tal:div[@type="keywords"][@xml:lang="en"]', namespaces=ns))
)
Par contre on peut voir que toutes les publications ont leur balise abstract et leurs mots clés présentes en anglais ET en francais. Cela va nous faciliter la tâche lors du scrapping même si ces balises sont vides. Si ces balises sont traduites, une comparaison ou une combinaison francais-anglais sera possible.
Pour se construire notre dataframe avec pandas on aura besoin d'extraire les données de chaque TEI. Pour cela nous allons créer une méthode python pour préciser le nom des colonnes du dataframe recherché et la facon dont chaque colonne sera parsé depuis le root du TEI
def xml_to_pandas(root, ns={}):
# create a dataframe with the columns as the keys in data_element_parsing
df = pd.DataFrame(
columns=[
'title',
'author',
'pubplace',
'date',
'lang',
'abstract_fr',
'keywords_fr',
'abstract_en',
'keywords_en',
'content'
]
)
# get and parse the data of each element (TEI)
for element in root.findall('./tal:TEI', ns):
# get all the data elements
data_element = {}
data_element['lang'] = element.attrib["{http://www.w3.org/XML/1998/namespace}lang"]
data_element['title'] = element.find('./tal:teiHeader/tal:fileDesc/tal:titleStmt/tal:title', ns).text
data_element['author'] = ', '.join([
x.text for x in element.findall('./tal:teiHeader/tal:fileDesc/tal:titleStmt/tal:author/tal:persName/tal:name', ns) if x is not None
])
data_element['pubplace'] = element.find('./tal:teiHeader/tal:fileDesc/tal:publicationStmt/tal:pubPlace', ns).text
data_element['date'] = element.find('./tal:teiHeader/tal:fileDesc/tal:publicationStmt/tal:date', ns).text
data_element['abstract_fr'] = element.find('./tal:text/tal:front/tal:div[@type="abstract"][@xml:lang="fr"]/tal:p', ns).text
data_element['abstract_en'] = element.find('./tal:text/tal:front/tal:div[@type="abstract"][@xml:lang="en"]/tal:p', ns).text
data_element['keywords_fr'] = element.find('./tal:text/tal:front/tal:div[@type="keywords"][@xml:lang="fr"]/tal:p', ns).text
data_element['keywords_en'] = element.find('./tal:text/tal:front/tal:div[@type="keywords"][@xml:lang="en"]/tal:p', ns).text
# get all the content on sections and subsections by concatenate
# everything (not matter the order) in a single variable
data_element['content'] = ''
sections = element.findall('.//tal:div[@type="section"]/tal:p', ns)
subsections = element.findall('.//tal:div[@type="subsection"]/tal:p', ns)
error = 0
for x in sections + subsections:
try:
data_element['content'] += x.text
except:
pass
# append the element as a row in the dataframe
df = df.append(data_element, ignore_index=True)
df.date = df.date.astype(int)
return df
corpus = xml_to_pandas(
corpus_root,
ns=ns
)
corpus
Avant de commencer nos analyses, il est important de savoir ce que l'on peut analyser. Nous allons alors vérifier quelles sont les données manquante. Ainsi nous pourrons savoir ce qu'il nous est simplement impossible d'analyser dû au manque de données. Nous vérifierons aussi le nombre de sample où toutes les données sont présentes ce qui nous laisserait les possibilités les plus vastes d'analyse et de synthèse. Nos analyses se concentrerons alors sur le nombre de données manquantes selon les langues, pour les titres, les abstracts, les keywords et les contenus. Nous vérifierons aussi la répartitions des publications dans le temps ce qui nous permettra de savoir si une analyse temporelle est possible.
Commençons par analyser les lignes où le content est vide si elles existent
print('Nombre de ligne avec un content vide : ', len(corpus[corpus.content == '']))
print('Dont publication francaise : ', len(corpus[(corpus.content == '') & (corpus.lang == 'fr')]))
print('Exemple :')
corpus[corpus.content == '']
On voit alors que 280 de nos publications dont 181 francaises ont été mal parsées. Autrement dit, le fichier XML a un défaut de format, des données manquantes ou la chaine de parsing que l'on a défini est mauvaise. Quoiqu'il en soit, on peut se dire que 280 sur 1602 est une que l'on peut se permettre pour nos analyses.
On peut aussi remarquer qu'il y des données manquantes pour les abstracts et les keywords où on remarque des "None". Il est intéressant de savoir en détail quelles sont ces données où les None sont présents.
miss_abs_fr = (corpus.abstract_fr == 'None')
miss_abs_en = (corpus.abstract_en == 'None')
miss_kwd_fr = (corpus.keywords_fr == 'None')
miss_kwd_en = (corpus.keywords_en == 'None')
print('Abstract francais manquant :', len(corpus[miss_abs_fr]))
print('Abstract anglais manquant :', len(corpus[miss_abs_en]))
print('Keywords francais manquant :', len(corpus[miss_kwd_fr]))
print('Keywords anglais manquant :', len(corpus[miss_kwd_en]))
miss_fr = miss_abs_fr & miss_kwd_fr
miss_en = miss_abs_en & miss_kwd_en
miss = miss_fr & miss_en
miss_and_content = (miss) & (corpus.content == '')
print('Dont Abstract et Keywords francais manquant :', len(corpus[miss_fr]))
print('Dont Abstract et Keywords anglais manquant :', len(corpus[miss_en]))
print('Dont Abstract et Keywords francais et anglais manquant :', len(corpus[miss]))
print('Dont Abstract et Keywords francais et anglais et content manquant :', len(corpus[miss_and_content]))
print('Exemple :')
corpus[miss_and_content]
On a un certain nombre de données manquantes que ce soit en francais, en anglais, et ce, sur l'abtract, les keywords et le content. Pour autant, nous ne supprimerons pas ces lignes car toutes contiennent un titre valable et doc sont utiles pour les analyses sur le titre.
corpus.groupby(by='date').date.count().plot(
title='Number of publication per year',
color='#8800ff'
)
plt.hlines(
30,
corpus.date.min(),
corpus.date.max(),
colors='#999999',
linestyles='dashed'
)
pass
On peut alors remarquer que les publications sont assez bien réparties dans le temps ce qui nous donnera la possibilité de faire nos analyses avec un axe temporel en fonction de notre avancement. (A noter que la droite horizontale est le seuil arbitraire placer à count=30 pour lequel nous estimons qu'une analyse est possible. Selon ce seuil, nous pourrons analyser les données de toutes les publications à partir de l'année 2000)
Nous avons pu voir qu'aucune ligne n'est à supprimer puisque toutes nos données comportent au moins un titre ou un content ou un asbtract/keywords. Donc tout est utilisable à analyse. Mais il sera nécessaire et indispensable de nettoyer nos données.
Pour les exemples qui vont suivres, nous allons utiliser les 5 premières lignes de abstract_fr en tant que visualisation de notre nettoyage car ces sont des paragraphes courts contrairement aux contenus et ne contiennent pas seulement une phrase contrairement aux titres.
pd.set_option('max_colwidth', 400) # default = 50
corpus_sample = corpus.iloc[:5][["abstract_fr", "lang"]]
corpus_sample
La première étape est plutot simple. On enlève les accents en passant simplement pas la case ascii.
def remove_accent(text):
norm = ucd.normalize('NFKD', text)
ascii = norm.encode('ascii', 'ignore')
text = ascii.decode('utf-8', 'ignore')
return text
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: remove_accent(x["abstract_fr"]),
axis=1
)
corpus_sample
On met tout en minuscule puisque "n" est différent de "N" ce qui n'a pas trop de sens considérant un mot (sauf nom propres que l'on considèreras comme non essentiel)
A noter que ce choix délibéré de ne pas compter les noms propre semble pertinent mais dans le cas de papier de recherche, les noms propres peuvent avoir leur importance plus que dans un texte quelconque.
def set_lower(text):
return text.lower()
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: set_lower(x["abstract_fr"]),
axis=1
)
corpus_sample
Nous arrivons à la dernière étape de nettoyage de nos données et celle-ci est plus complexe. Nous utiliserons les expressions régulières pour traiter d'un coup unique plusieurs nettoyages.
replacement_patterns = [
# remove extra lines
(r'\n', ''),
# remove special characters
(r'[^a-zA-Z\d\s]', ' '),
# remove extra space
(r'\s{2,}', ' '),
]
replacement_patterns_en = [
# gestion de quelques contractions
(r'n\'', 'ne '),
(r'can\'t', 'cannot'),
(r'i\'m', 'i am'),
(r'ain\'t', 'is not'),
(r'(\w+)\'ll', r'\g<1> will'),
(r'(\w+)n\'t', r'\g<1> not'),
(r'(\w+)\'ve', r'\g<1> have'),
(r'(\w+)\'s', r'\g<1> is'),
(r'(\w+)\'re', r'\g<1> are'),
(r'(\w+)\'d', r'\g<1> would'),
] + replacement_patterns
replacement_patterns_fr = [
# remplacement des contractions avec seulement un "e" a la fin
# exemple : "qu'ils" devient "que ils"
(r'(\w+)\'', r'\g<1>e '),
] + replacement_patterns
class RegexpReplacer(object):
def __init__(self):
self.patterns = {
'en': [(re.compile(regex), repl) for (regex, repl) in replacement_patterns_en],
'fr': [(re.compile(regex), repl) for (regex, repl) in replacement_patterns_fr]
}
def __call__(self, text, lang):
s = text
for (pattern, repl) in self.patterns[lang]:
s = re.sub(pattern, repl, s)
return s
replace_regex = RegexpReplacer()
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: replace_regex(x["abstract_fr"], "fr"),
axis=1
)
corpus_sample
On va supprimer aussi les stopwords. Il faut aussi, comme dans les cas des regex, utiliser un set de stopwords qui sera propre à la langue dans laquelle est le texte. En l'occurence on télécharger les stopwrods de la langue anglaise et de la langue francaise
class StopWordsRemover(object):
def __init__(self, regex_split=r'[a-zA-Z]+'):
self.stopwords = {
'en': set(stopwords.words('english')),
'fr': set(stopwords.words('french'))
}
self.tokenizer = RegexpTokenizer(regex_split)
def __call__(self, text, lang):
text = self.tokenizer.tokenize(text)
text = ' '.join([x for x in text if not x in self.stopwords[lang]])
return text
remove_stopwords = StopWordsRemover()
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: remove_stopwords(x["abstract_fr"], "fr"),
axis=1
)
corpus_sample
Enfin notre dernière fonction permet de lemmatizer un texte. Encore une fois, il nous faut une possibilité francaise et une autre anglaise
class Lemmatizer(object):
def __init__(self, regex_split=r'[a-zA-Z]+'):
self.lemmatizer = {
'en': spacy.load('en'),
'fr': spacy.load('fr')
}
self.tokenizer = RegexpTokenizer(regex_split)
def __call__(self, text, lang):
text = ' '.join([x.lemma_ for x in self.lemmatizer[lang](text)])
return text
lemmatize = Lemmatizer()
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: lemmatize(x["abstract_fr"], "fr"),
axis=1
)
corpus_sample
En regardant de plus près notre lemmatization, au jugé, nous trouvons qu'elle n'est pas superbe. Elle altère mal le texte francais car trop peu d'équivalents sont trouvés dans ce vocabulaire trop technique et peu courant qu'est celui de papier de recherche. Alors nous décidons finalement de ne pas utiliser de lemmatization au risque de nuire à nos données plus qu'autre chose
Enfin, nous aurons besoin de "Tokeniser" nos phrases après nettoyage. Nous faisons cette fonction à part pour deux raisons :
regex_split = r'[a-zA-Z]+'
tokenizer = RegexpTokenizer(regex_split)
def n_gram_tokenize(text, n, n_gram_jointure=None):
# tokenization
text_tokenized = tokenizer.tokenize(text)
# n-gram
text_tokenized = [text_tokenized[i:i+n] for i in range(len(text_tokenized)- n + 1)]
# jointure if asked
if n_gram_jointure is not None:
text_tokenized = [n_gram_jointure.join(x) for x in text_tokenized]
return text_tokenized
corpus_sample["abstract_fr"] = corpus_sample.apply(
lambda x: n_gram_tokenize(x["abstract_fr"], 2, n_gram_jointure=' '),
axis=1
)
corpus_sample
Il ne reste plus qu'à regrouper nos fonctions pour les appliquer par colonne facilement
def clean_column(df, col, lang, lang_is_col=False, n_gram=None, n_gram_jointure=None, inplace=False):
if not inplace:
df = df.copy()
# avoid exceptions of None values replacing them with "None" values
df[col] = df.apply(
lambda x:x[col] if x[col] is not None else 'None',
axis=1
)
# remove accents
df[col] = df.apply(
lambda x:remove_accent(x[col]),
axis=1
)
# to lower
df[col] = df.apply(
lambda x:set_lower(x[col]),
axis=1
)
# replace regex
df[col] = df.apply(
lambda x:replace_regex(x[col], x[lang] if lang_is_col else lang),
axis=1
)
# remove stopwords
df[col] = df.apply(
lambda x:remove_stopwords(x[col], x[lang] if lang_is_col else lang),
axis=1
)
# n gram
if n_gram is not None:
df[col] = df.apply(
lambda x:n_gram_tokenize(x[col], n_gram, n_gram_jointure),
axis=1
)
return df
Avant de faire toutes nos analyses, on aura besoin donc de notre corpus entièrement nettoyé. Mais nous gardons ce copurs sans tokenization pour en essayer des différentes.
pd.set_option('max_colwidth', 50) # default = 50
clean_column(corpus, 'title', 'lang', lang_is_col=True, n_gram=None, inplace=True)
clean_column(corpus, 'content', 'lang', lang_is_col=True, n_gram=None, inplace=True)
clean_column(corpus, 'abstract_fr', 'fr', n_gram=None, inplace=True)
clean_column(corpus, 'abstract_en', 'en', n_gram=None, inplace=True)
clean_column(corpus, 'keywords_fr', 'fr', n_gram=None, inplace=True)
clean_column(corpus, 'keywords_en', 'en', n_gram=None, inplace=True)
corpus
Nous pouvons alors à l'aide de notre corpus nettoyé, concevoir nos token. Pour cette étude nous avons décidé de ne traiter que des unigram et de bigram. Nous aurions pu aller plus loin dans les ngram mais plus le ngram est important et plus la complexité l'est aussi. Nous avons un tout petit dataset donc il se peut même que les bigram soient de trop. Nous verrons cela par la suite.
unigram_corpus = corpus.copy()
bigram_corpus = corpus.copy()
cols = ['title', 'abstract_fr', 'abstract_en', 'keywords_fr', 'keywords_en', 'content']
for col in cols:
unigram_corpus[col] = unigram_corpus.apply(lambda x:n_gram_tokenize(x[col], 1, ' '), axis=1)
bigram_corpus[col] = bigram_corpus.apply(lambda x:n_gram_tokenize(x[col], 2, ' '), axis=1)
unigram_corpus
bigram_corpus
La loi de Zipf est une observation empirique concernant la fréquence des mots dans un texte. La loi de Zipf dit que le nombre d'occurence d'un mot dans un corpus est inversement proportionnel à son rang. C'est une analyse intéressante selon nous. On voulait savoir si notre corpus de publication respecte cette distribution.
On se créer d'abord une méthode qui nous permet de nous retourner pour une colonne d'un dataframe donné, la liste des mots ordonnés par fréquence d'apparition et la liste de fréquence associée.
from nltk.probability import FreqDist
def zipf(df, col, df_mask=None, to_freq=False):
if df_mask is not None:
df = df[df_mask]
# get the number of occurences of each words
words = [word for value in df[col].values for word in value]
fdist = FreqDist(words)
# sort these words according to their occurence
sorted_words = sorted(fdist, key=fdist.get, reverse=True)
# get the frequence of these words
sorted_occurences = np.array([fdist[word] for word in sorted_words])
if to_freq:
sorted_occurences = sorted_occurences / np.sum(sorted_occurences)
return sorted_words, sorted_occurences
cols = ['title', 'title', 'content', 'content', 'abstract_fr', 'abstract_en', 'keywords_fr', 'keywords_en']
lang_filters = ['fr', 'en', 'fr', 'en', 'fr', 'en', 'fr', 'en']
for col, lang in zip(cols, lang_filters):
# mask on langage
mask = unigram_corpus.lang == lang
# get the words and frequencies in this columns for this langagz
words, freqs = zipf(unigram_corpus, col, mask, to_freq=True)
# plot it
plt.plot(range(freqs.shape[0]), freqs, label=f'{col} (lang={lang})')
# add log scales to better visualize the Zip's law if exists
plt.xscale('log')
plt.yscale('log')
# Add title and axis names and legends
plt.title('Zipf\'s law on TALN corpus with unigram tokens')
plt.xlabel('log(rank_of_word)')
plt.ylabel('log(fred_of_word)')
plt.legend()
pass
cols = ['title', 'title', 'content', 'content', 'abstract_fr', 'abstract_en', 'keywords_fr', 'keywords_en']
lang_filters = ['fr', 'en', 'fr', 'en', 'fr', 'en', 'fr', 'en']
for col, lang in zip(cols, lang_filters):
# mask on langage
mask = bigram_corpus.lang == lang
# get the words and frequencies in this columns for this langagz
words, freqs = zipf(bigram_corpus, col, mask,to_freq=True)
# plot it
plt.plot(range(freqs.shape[0]), freqs, label=f'{col} (lang={lang})')
# add log scales to better visualize the Zip's law if exists
plt.xscale('log')
plt.yscale('log')
# Add title and axis names and legends
plt.title('Zipf\'s law on TALN corpus with bigram tokens')
plt.xlabel('log(rank_of_word)')
plt.ylabel('log(fred_of_word)')
plt.legend()
pass
Il est très interessant de voir que en francais comme en anglais, pour le titre, l'abstract, le contenu ou les keywords, la répartition de chaque sous corpus semble suivre la loi de Zipf. Il semble (a l'oeil nu) qu'il y ait une forte relation proportionnelle entre le rang de chaque mot et sa fréquence, comme on peut le voir sur les deux graphs ci-dessus à echelle logarithmique. Par ailleurs, pas très étonnant mais cela fonctionne aussi bien sur les unigram que sur les bigram. On ne peut pas en tirer beaucoup d'informations intéressante avec cette loi mais au moins on sait que notre corpus, bien que technique et spécialisé dans le NLP, a une répartition de ses mots telle que la répartition des mots au sein d'une langue donnée.
Dans cette partie, non loin de la précédente, nous allons essayer de comprendre quels sont les mots les plus récurrents de ce corpus. Nous ne traiterons que les titres, abstract et keywords car nous considérons que le contenu sera résumé par ces parties et n'apportera pas d'information supplémentaires quant à la pertinence des mots les plus récurrents.
Nous allons donc nous créer une table dans laquelle nous ne garderons que les articles francais et nous y ajouterons une colonne qui concatène le titre, l'abstract et les keywords
pd.set_option('max_colwidth', 300) # default = 50
unigram_corpus_fr = unigram_corpus[unigram_corpus.lang == 'fr']
# "tak" for title, abstract and keywords
cols = ['title', 'abstract_fr', 'keywords_fr']
unigram_corpus_fr['tak'] = unigram_corpus_fr[cols].apply(
# flatten list of list x
lambda x: [inner for outer in x for inner in outer]
, axis=1
)
unigram_corpus_fr[cols + ['tak']]
On se créer une petite méthode pour que ça aille plus vite ensuite.
def get_top(df, col, n, df_mask=None, to_freq=False):
words, freqs = zipf(df, col, df_mask, to_freq)
# avoid index out of range
n = min(len(words), n)
# get the top ones
words = words[:n]
freqs = freqs[:n]
return words, freqs
top_n = 100
top_words, top_freqs = get_top(unigram_corpus_fr, 'tak', top_n, to_freq=True)
plt.title(f'Top des {len(top_words)} mots les plus courants dans notre corpus (Unigram on TAK column filtered with lang="fr")')
plt.bar(range(len(top_words)), top_freqs, color='#2f9599')
plt.xticks(range(len(top_words)), top_words, rotation='vertical')
pass
On peut voir que lors de notre plot des 100 mots les plus fréquents on se retrouve avec des mots que l'on considère comme des stopwords. On note :
donc nous n'allons pas les retirer mais on peut ici voir qu'ils n'apportent que peu d'information quant à notre étude et on peut alors comprendre les limites des telles approches de nettoyages de texte avec des stopwords. La limite est arbitraire et ce que nltk considère comme des stopwords n'est pas ce que nous considérons comme dse stopwords.
Par contre nous pouvons remarquer déjà quelque chose à l'oeil nu. C'est que le vocabulaire utilisé dans ce corpus est très technique et spécialisé. On remarque facilement lorsque l'on connait ces techniques qu'ils s'agit de vocabulaire issue du traitement de langage naturel (NLP).
top_n = 100
top_words, top_freqs = get_top(unigram_corpus_fr, 'tak', top_n, to_freq=True)
# remove other custom stop_words
other_stopwords = ['a', 'cet', 'cette']
stop_idx = np.where(np.in1d(top_words, other_stopwords))
top_words = np.delete(top_words, stop_idx)
top_freqs = np.delete(top_freqs, stop_idx)
plt.title(f'Frequence des {len(top_words)} mots les plus courants dans notre corpus (Unigram on TAK column filtered with lang="fr")')
plt.bar(range(len(top_words)), top_freqs, color='#2f9599')
plt.xticks(range(len(top_words)), top_words, rotation='vertical')
pass
Comme on l'avait dit précédemment nous avons conclu que nous pourrions analyser le corpus temporellement à partir de l'année 2000. Alors nous pensons, que dans cette logique, il est intéressant de regarder l'évolution des 100 mots les plus courant (97 sans les 3 stop words que nous avons retirés) au cours du temps.
# get all the years in the corpus from 2000
years = np.array([year for year in unigram_corpus_fr.date.unique() if year > 1999])
def get_words_freq(df, col, words_list, df_mask=None):
if df_mask is not None:
df = df[df_mask]
# get the number of occurences of each words
words = [word for value in df[col].values for word in value]
fdist = FreqDist(words)
# get freqs of the given words
sum_words_count = sum(fdist.values())
freqs = [fdist[word] / sum_words_count for word in words_list]
return freqs
top_freqs = get_words_freq(unigram_corpus_fr, 'tak', top_words)
for i in range(10):
print(f'word "{top_words[i]:12}" of frequence {top_freqs[i]:.4f}')
On retrouve bien nos valeurs que l'on voit sur notre graphique donc notre fonction est bonne et peut être utilisée avec un masque par année.
def plot_add_regression_line(x, y, color):
m, b = np.polyfit(x, y, 1)
plt.plot(x, m * x + b, '-.', color=plt.default_colors[i])
# Create a dict to store top words frequencies by year
top_words_all = top_words
top_freqs_all = get_words_freq(unigram_corpus_fr, 'tak', top_words)
top_freqs_by_year = {word: [] for word in top_words_all}
# iterate by year to get all the word frequencie within this year
for year in years:
mask_year = unigram_corpus_fr.date == year
# get the frequencies
freqs_year = get_words_freq(unigram_corpus_fr, 'tak', top_words, df_mask=mask_year)
for word, freq in zip(top_words_all, freqs_year):
top_freqs_by_year[word].append(freq)
exemple_words = top_words_all[:9:3]
for i, word in enumerate(exemple_words):
plt.plot(years, top_freqs_by_year[word], label=word, color=plt.default_colors[i])
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Maintenant que nous avons les evolutions de chaque mot au cours du temps nous pouvons nous concentrer sur une simple regression linéaire et estimer alors lesquels mots sont de plus en plus fréquents et inversement
def get_slope(x, y):
slope, intercept = np.polyfit(x, y, deg=1)
return slope
dict_slopes = {
word: get_slope(years, top_freqs_by_year[word])
for word in top_words_all
}
exemple_words = top_words_all[:9:3]
for word in exemple_words:
print(f'The word "{word:8}" has a slope of {dict_slopes[word]:.5f}')
On peut donc savoir par exemple quels sont les 5 mots avec la meilleure évolution en terme de fréquence
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
reverse=True
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Le résultat n'est pas tellement surprenant mais c'est amusant de remarquer que les sujets d'actualité ressortent parfaitement sur ce graphique. Depuis les 10 dernières années, les enejeux majeurs en NLP sont l'apprentissage automatique, la détection de toute sorte (OCR par exemple) et le manque de donnée qui peut ressortir dans les mots "corpus" et surtout "annotation"
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
A l'inverse, même s'il est difficile de déterminer pourquoi ces mots sont de moins en moins utiliser mais nous pensons sans trop nous mouiller pouvoir tous ces mots sont de moins en moins utilisés parce que l'enjeu d'apprentissage peut remplacer "l'analyse", les "systèmes", les "grammaires" en non supervisé. Par contre pour "dialogue" on peut penser que ça a été remplacé par des mots tels que "chatbot" ou "conversation" qui sont plus en vogue
(Comme pour la section Francais)
Nous allons donc nous créer une table dans laquelle nous ne garderons que les articles francais et nous y ajouterons une colonne qui concatène le titre, l'abstract et les keywords
pd.set_option('max_colwidth', 300) # default = 50
unigram_corpus_en = unigram_corpus[unigram_corpus.lang == 'en']
# "tak" for title, abstract and keywords
cols = ['title', 'abstract_en', 'keywords_en']
unigram_corpus_en['tak'] = unigram_corpus_en[cols].apply(
# flatten list of list x
lambda x: [inner for outer in x for inner in outer]
, axis=1
)
unigram_corpus_en[cols + ['tak']]
top_n = 100
top_words, top_freqs = get_top(unigram_corpus_en, 'tak', top_n, to_freq=True)
plt.title(f'Frequence des {len(top_words)} mots les plus courants dans notre corpus (Unigram on TAK column filtered with lang="en")')
plt.bar(range(len(top_words)), top_freqs, color='#2f9599')
plt.xticks(range(len(top_words)), top_words, rotation='vertical')
pass
Pareil que pour le cas des données en francais, nous pouvons remarquer déjà quelque chose à l'oeil nu. Le vocabulaire utilisé dans ce corpus est très technique et spécialisé en NLP.
# remove other custom stop_words
other_stopwords = ['none']
stop_idx = np.where(np.in1d(top_words, other_stopwords))
top_words = np.delete(top_words, stop_idx)
top_freqs = np.delete(top_freqs, stop_idx)
(Comme pour le cas francais)
Nous avons dressé le top 100 des mots les plus fréquents et nous allons analyser leur évolutions.
# get all the years in the corpus from 2000
years = np.array([year for year in unigram_corpus_en.date.unique() if year > 1999])
# Create a dict to store top words frequencies by year
top_words_all = top_words
top_freqs_all = get_words_freq(unigram_corpus_en, 'tak', top_words)
top_freqs_by_year = {word: [] for word in top_words_all}
# iterate by year to get all the word frequencie within this year
for year in years:
mask_year = unigram_corpus_en.date == year
# get the frequencies
freqs_year = get_words_freq(unigram_corpus_en, 'tak', top_words, df_mask=mask_year)
for word, freq in zip(top_words_all, freqs_year):
top_freqs_by_year[word].append(freq)
exemple_words = top_words_all[:9:3]
for i, word in enumerate(exemple_words):
plt.plot(years, top_freqs_by_year[word], label=word)
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="en")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Maintenant que nous avons les evolutions de chaque mot au cours du temps nous pouvons nous concentrer sur une simple regression linéaire et estimer alors lesquels mots sont de plus en plus fréquents et inversement
dict_slopes = {
word: get_slope(years, top_freqs_by_year[word])
for word in top_words_all
}
exemple_words = top_words_all[:9:3]
for word in exemple_words:
print(f'The word "{word:8}" has a slope of {dict_slopes[word]:.5f}')
On peut donc savoir par exemple quels sont les 5 mots avec la meilleure évolution en terme de fréquence
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
reverse=True
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="en")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Ici on a un corpus francais de texte traduits en anglais donc il n'est pas étonnant de retrouver le mot "french" et on retrouve les mots "corpora", "document".
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Unigram on TAK column filtered with lang="en")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
C'est intéressant de remarquer que l'on retrouve ici aussi le mot "grammars".
Enfin nous analysons les bigram pour le corpus en francais. Nous ne le ferons pas pour le corpus en anglais puisqu'il est plus lacunaire donc la complexité avec des bigram en anglais ne serait pas très significative. Ceci étant dis, recommencons le process que l'on a déjà entammé par deux fois ci-dessus
(Comme pour la section Francais)
Nous allons donc nous créer une table dans laquelle nous ne garderons que les articles francais et nous y ajouterons une colonne qui concatène le titre, l'abstract et les keywords
pd.set_option('max_colwidth', 300) # default = 50
bigram_corpus_fr = bigram_corpus[bigram_corpus.lang == 'fr']
# "tak" for title, abstract and keywords
cols = ['title', 'abstract_fr', 'keywords_fr']
bigram_corpus_fr['tak'] = bigram_corpus_fr[cols].apply(
# flatten list of list x
lambda x: [inner for outer in x for inner in outer]
, axis=1
)
bigram_corpus_fr[cols + ['tak']]
top_n = 100
top_words, top_freqs = get_top(bigram_corpus_fr, 'tak', top_n, to_freq=True)
plt.title(f'Frequence des {len(top_words)} mots les plus courants dans notre corpus (Bigram on TAK column filtered with lang="fr")')
plt.bar(range(len(top_words)), top_freqs, color='#2f9599')
plt.xticks(range(len(top_words)), top_words, rotation='vertical')
pass
Pareil que pour le cas des données en francais en unigram, nous pouvons remarquer que le vocabulaire utilisé dans ce corpus est très technique et spécialisé en NLP. Mais il est beaucoup plus technique et spécialisé en bigram qu'en unigram grâce à l'association des mots. Ici, plus aucun doute, il s'agit à coup sûr d'un corpus de publication de NLP dont le premier bigram est d'ailleurs "cet article"
(Comme pour le cas francais)
Nous avons dressé le top 100 des mots les plus fréquents et nous allons analyser leur évolutions.
# get all the years in the corpus from 2000
years =np.array([year for year in bigram_corpus_fr.date.unique() if year > 1999])
# Create a dict to store top words frequencies by year
top_words_all = top_words
top_freqs_all = get_words_freq(bigram_corpus_fr, 'tak', top_words)
top_freqs_by_year = {word: [] for word in top_words_all}
# iterate by year to get all the word frequencie within this year
for year in years:
mask_year = bigram_corpus_fr.date == year
# get the frequencies
freqs_year = get_words_freq(bigram_corpus_fr, 'tak', top_words, df_mask=mask_year)
for word, freq in zip(top_words_all, freqs_year):
top_freqs_by_year[word].append(freq)
exemple_words = top_words_all[:9:3]
for i, word in enumerate(exemple_words):
plt.plot(years, top_freqs_by_year[word], label=word)
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Bigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Maintenant que nous avons les evolutions de chaque mot au cours du temps nous pouvons nous concentrer sur une simple regression linéaire et estimer alors lesquels mots sont de plus en plus fréquents et inversement
dict_slopes = {
word: get_slope(years, top_freqs_by_year[word])
for word in top_words_all
}
exemple_words = top_words_all[:9:3]
for word in exemple_words:
print(f'The word "{word:8}" has a slope of {dict_slopes[word]:.5f}')
On peut donc savoir par exemple quels sont les 5 mots avec la meilleure évolution en terme de fréquence
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
reverse=True
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Bigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Beaucoup plus intéressant que pour les unigram, ici on voit carrément la mentalité des publications de recherche ressortir (en extrapolant sur notre vécu) Il est plus qu'évident que la comparaison à l'état de l'art est une tendance qui est de plus en plus forte. De même pour les réseaux neuronnaux et la recherche de sémantique dans du non supervisé et d'entité dans du non structuré (ce qui est d'ailleurs permis par les réseaux neuronaux).
dict_slopes = {
word: slope
for word, slope in sorted(
dict_slopes.items(),
key=lambda item: item[1],
)
}
n_best = 5
for i, (word, slope) in enumerate(dict_slopes.items()):
if not i < n_best:
break
plt.plot(years, top_freqs_by_year[word], label=f'{word} (slope={slope:6f})')
# add regression line
plot_add_regression_line(years, top_freqs_by_year[word], plt.default_colors[i])
plt.title('Frequence of words over years (Bigram on TAK column filtered with lang="fr")')
plt.xticks(years, years)
plt.xlabel('Years')
plt.ylabel('Frequence')
plt.legend()
pass
Pareil que la dernière fois, il est difficile de comprendre pourquoi tel ou tel bigram baisse. Mais c'est amusant ce remarquer que le "peut être" perd sa place dans les publications de recherche. On peut tout de même faire aussi l'extrapolation faite plus haut disant que l'analyse syntaxique disparait au profit de l'automatisation et l'apprentissage automatique
Maintenant que nous avons un apercu sur les mots les plus courants et les évolutions respectives sur les titres, les abstracts, les keywords, pour les unigram, les bigrams, en francais et anglais, nous voulons essayer de créer des clusters de ces articles. Nous sommes conscients que lors du cours nous n'avons pas vu le K-Means clustering mais nous voulions l'utiliser pour le comparer par la suite à ce que l'on a fait en cours. Nous ne traiterons plus ni le titre, ni les keywords, ni les abstracts mais nous traiterons les contenus à partir de maintenant. Il permettrons de pouvoir utiliser convenablement cette algortihme sans trop se soucier de la complexité du vocabulaire comparé à la taille de notre corpus.
pd.set_option('max_colwidth', 100) # default = 50
content_fr = corpus[corpus.lang == 'fr'][["content"]]
content_fr
# Applying TFIDF
vectorizer = TfidfVectorizer(ngram_range=(1,1))
tfidf = vectorizer.fit_transform(content_fr["content"]).toarray()
tfidf.shape
La taille de (1502, 140497) est très très importante. Ce qui implique que si nous voulons créer un dataframe avec autant de valeurs alors nous serons confrontés à un problème de RAM dans le notebook. Réduisons alors le vecteur au top 5000 unigram du corpus
max_features = 5000
vectorizer = TfidfVectorizer(ngram_range=(1,1), max_features=max_features)
tfidf = vectorizer.fit_transform(content_fr["content"]).toarray()
content_fr['tfidf'] = tfidf.tolist()
content_fr
Pour choisir notre k (hyperparamètre du K-Means), nous allons procéder à une elbow method. Pour faire simple, il s'agit de tester toutes les valeurs de k sur un intervalle (nous choissirons de 1 à 15) et d'évaluer à l'oeil nu quel valeur de k semble la plus pertinente. Généralement on choisit comme valeur de k, la valeur à laquelle la courbe est en forme de coude, d'où le nom de la méthode.
%%time
sum_squared_dists = []
k_range = range(1,15)
for k in k_range:
print(f'Performing {k}-Means...')
kmeans = KMeans(
n_clusters=k,
n_init=10, # number of different initialization (keep best)
max_iter=200,
random_state=42
)
kmeans = kmeans.fit(np.array(list(content_fr.tfidf)))
sum_squared_dists.append(kmeans.inertia_)
print(f'\tInertia : {kmeans.inertia_:8.2f} after {kmeans.n_iter_:3} iterations')
plt.plot(k_range, sum_squared_dists, '-o')
plt.xlabel('k')
plt.ylabel('Sum of squared distances')
plt.title('Elbow Method For Optimal k (Unigram on content column filtered with lang="fr")')
plt.show()
En l'occurence, il est ici difficile de choisir une bonne valeur de k. On peut remarquer qu'avec l'augmentation de k, il n'y a pas de réelle et forte diminution de l'erreur au carrée. D'ailleurs, on ne remarque pas de coude sur cette courbe alors nous choissirons pour la suite la valeur de k=6.
On fit alors notre algo KMeans pour trouver k=6 clusters dans nos contenus.
%%time
true_k = 6
kmeans = KMeans(
n_clusters=true_k,
init='k-means++',
max_iter=200,
n_init=20,
random_state=42
)
kmeans.fit(np.array(list(content_fr.tfidf)))
content_fr['cluster'] = list(kmeans.labels_)
content_fr
Maintentant que nous avons crée k= clusters dans nos contenus, nous pouvons alors faire une visualisation sous forme de nuage de mot pour chacun de ces clusters.
plt.rcParams['figure.figsize'] = [4, 2]
for k in range(0, true_k):
k_content = content_fr[content_fr.cluster==k]
text = k_content['content'].str.cat(sep=' ')
wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white").generate(text)
plt.figure()
plt.imshow(wordcloud, interpolation="bilinear")
plt.title(f'Wordcould of cluster {k+1} (Unigram on content column filtered with lang="fr")')
plt.axis("off")
plt.show()
plt.rcParams['figure.figsize'] = [11, 4]
On peut alors constater, qu'en majorité, ce sont les mêmes mots qui reviennent dans notre contenu. L'analyse KMeans sur le contenu n'est pas aussi pertinente que nous l'espérions. On peut tout de même remarquer quelques clusters :
Finalement, nous voulions traiter nos contenus et les analyser avec KMeans mais cela s'est révélé peu satisfaisant selon nous. Alors on est en droit de se demander si cet algorithme est pertinent pour ce corpus ou si c'est le contenu qui est trop complexe pour l'algorithme. Pour répondre à cette question, nous allons faire la même analyse mais avec les titres et non les contenus.
pd.set_option('max_colwidth', 100) # default = 50
title_fr = corpus[corpus.lang == 'fr'][["title"]]
title_fr
# Applying TFIDF
vectorizer = TfidfVectorizer(ngram_range=(1,1))
tfidf = vectorizer.fit_transform(title_fr["title"]).toarray()
tfidf.shape
La taille de (1502, 2713) est clairement moins importante que celle avec le contenu de (1502, 140497) alors nous ne mettons pas de max_features=5000. Cela nous rassure dans le sens où nous cherchions à réduire la complexité du vocabulaire pour utiliser .
vectorizer = TfidfVectorizer(ngram_range=(1,1))
tfidf = vectorizer.fit_transform(title_fr["title"]).toarray()
title_fr['tfidf'] = tfidf.tolist()
title_fr
%%time
sum_squared_dists = []
k_range = range(1,15)
for k in k_range:
print(f'Performing {k}-Means...')
kmeans = KMeans(
n_clusters=k,
n_init=20, # number of different initialization (keep best)
max_iter=200,
random_state=42
)
kmeans = kmeans.fit(np.array(list(title_fr.tfidf)))
sum_squared_dists.append(kmeans.inertia_)
print(f'\tInertia : {kmeans.inertia_:8.2f} after {kmeans.n_iter_:3} iterations')
plt.plot(k_range, sum_squared_dists, '-o')
plt.xlabel('k')
plt.ylabel('Sum of squared distances')
plt.title('Elbow Method For Optimal k (Unigram on title column filtered with lang="fr")')
plt.show()
Comme pour le contenu, on remarque que la méthode du coude n'est pas très efficace pour ces données. Il y a une erreur inversement proportionnelle à la valeur de k. Alors dans un soucis de comparaison (arbitraire), nous resterons sur k=6.
%%time
true_k = 6
kmeans = KMeans(
n_clusters=true_k,
init='k-means++',
max_iter=200,
n_init=20,
random_state=42
)
kmeans.fit(np.array(list(title_fr.tfidf)))
title_fr['cluster'] = list(kmeans.labels_)
title_fr
plt.rcParams['figure.figsize'] = [4, 2]
for k in range(0, true_k):
k_content = title_fr[title_fr.cluster==k]
text = k_content['title'].str.cat(sep=' ')
wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white").generate(text)
plt.figure()
plt.imshow(wordcloud, interpolation="bilinear")
plt.title(f'Wordcould of cluster {k+1} (Unigram on title column filtered with lang="fr")')
plt.axis("off")
plt.show()
plt.rcParams['figure.figsize'] = [11, 4]
Finalement, KMeans sur les titres est beaucoup plus pertinent que sur le contenu. Ici on peut retrouver 6 thèmes bien différents pour chacun de ces 6 clusters :
On peut alors conclure en un sens que la complexité du vocabulaire joue un rôle primordial sur l'efficience du KMeans. Donc le titre est plus pertinent que le contenu dans ce cas. Alors en ce sens nous n'étudierons pas les bigrams pour la partie KMeans. De la même facon avec seulement 100 publications anglaises, la complexité du vocabulaire par rapport à la taille du jeu de donnée sera peut être trop importante pour utiliser un KMeans. Mais essayons...
Pour finir nos comparaison, et puisque les bigram rendront le vocabulaire trop complexe pour utiliser un KMeans, nous regarderons seulement si une comparaison est faisable pour les titres en anglais unigram par rapport aux titre unigram en francais. Nous chois
# Get TFIDF
pd.set_option('max_colwidth', 100) # default = 50
title_en = corpus[corpus.lang == 'en'][["title"]]
# Applying TFIDF
vectorizer = TfidfVectorizer(ngram_range=(1,1))
tfidf = vectorizer.fit_transform(title_en["title"]).toarray()
title_en['tfidf'] = tfidf.tolist()
# elbow method
sum_squared_dists = []
k_range = range(1,15)
for k in k_range:
kmeans = KMeans(
n_clusters=k,
n_init=20, # number of different initialization (keep best)
max_iter=200,
random_state=42
)
kmeans = kmeans.fit(np.array(list(title_en.tfidf)))
sum_squared_dists.append(kmeans.inertia_)
plt.plot(k_range, sum_squared_dists, '-o')
plt.xlabel('k')
plt.ylabel('Sum of squared distances')
plt.title('Elbow Method For Optimal k (Unigram on title column filtered with lang="en")')
plt.show()
Encore une fois, il n'y a pas de coude apparent alors nous allons abitrairement partir sur k=3 car nous n'avons que 100 lignes de titres en anglais. Donc partir sur plus que k=3 serait un peu de l'overfit selon nous.
# Get the k clusters with KMeans
true_k = 3
# init
kmeans = KMeans(
n_clusters=true_k,
init='k-means++',
max_iter=200,
n_init=20,
random_state=42
)
# fit
kmeans.fit(np.array(list(title_en.tfidf)))
# set results
title_en['cluster'] = list(kmeans.labels_)
title_en.groupby(by='cluster').count()[['title']]
Même avec peu de données, on peut dire que nos données ne sont pas trop mal réparties même si le cluster 0 prend plus de la moitié des titres anglais.
# word Clouds
plt.rcParams['figure.figsize'] = [4, 2]
for k in range(0, true_k):
k_content = title_en[title_en.cluster==k]
text = k_content['title'].str.cat(sep=' ')
wordcloud = WordCloud(max_font_size=50, max_words=100, background_color="white").generate(text)
plt.figure()
plt.imshow(wordcloud, interpolation="bilinear")
plt.title(f'Wordcould of cluster {k+1} (Unigram on title column filtered with lang="en")')
plt.axis("off")
plt.show()
plt.rcParams['figure.figsize'] = [11, 4]
C'est intéressant de remarque que l'on a les mêmes styles de clustes avec les titres en anglais et k=3 au lieu de k=6.
data = list(unigram_corpus_fr[unigram_corpus_fr.lang == 'fr'].tak)
# get the dictionary of the corpus as token ids
corpus_dict = gensim.corpora.Dictionary(data)
# get the bag-of-words (as tuple (token_id, token_count))
corpus_bow = [corpus_dict.doc2bow(word) for word in data]
Pour avoir une LDA effective nous n'allons pas choisir les hyperparamètres au hasard. Nous allons effectuer un entrainement rapide (5 passes à chaque fois) pour chaque combinaison d'hyperparameètres de notre grille de recherche. Notre grille de recherche comprend 144 combinaisons différentes alors nous allons le faire tourner une seule fois et sauvegarder les résultats.
Pour résumer l'intuition et la signification de ces différents hyperparamètres :
# supporting function
def compute_coherence_values(corpus, id2word, texts, k, a, b):
lda_model = gensim.models.LdaMulticore(
corpus=corpus,
id2word=id2word,
num_topics=k,
random_state=42,
passes=5,
alpha=a,
eta=b
)
coherence_model_lda = gensim.models.CoherenceModel(model=lda_model, texts=texts, dictionary=id2word, coherence='c_v')
return coherence_model_lda.get_coherence()
%%time
RE_TRAIN_GRID_SEARCH = False
if RE_TRAIN_GRID_SEARCH:
# Topics range
topics_range = range(2, 11, 1)
# Alpha parameter
alpha = list(np.arange(0.01, 1, 0.3))
# Beta parameter
beta = list(np.arange(0.01, 1, 0.3))
# Validation sets
num_of_docs = len(data)
model_results = {
'Topics': [],
'Alpha': [],
'Beta': [],
'Coherence': []
}
# iterate through number of topics
for k in topics_range:
# iterate through alpha values
for a in alpha:
# iterare through beta values
for b in beta:
print(f'[IN RUN] : Num Topics = {k}, alpha = {a:.3f}, beta = {b:.3f}')
# get the coherence score for the given parameters
cv = compute_coherence_values(
corpus_bow,
corpus_dict,
data,
k=k,
a=a,
b=b
)
# Save the model results
model_results['Topics'].append(k)
model_results['Alpha'].append(a)
model_results['Beta'].append(b)
model_results['Coherence'].append(cv)
results = pd.DataFrame(model_results)
results.to_csv(os.path.join(save_dir, 'LDA_Grid_Search.csv'), index=False)
L'entrainement avec les hyperparamètres est très très long (43min 41s) alors on a sauvargé les résultats pour ne pas avoir à faire tourner les entrainements à nouveau.
results = pd.read_csv(os.path.join(save_dir, 'LDA_Grid_Search.csv'))
results
On peut alors essayer de voir quels sont les meilleurs hyperparamètres dans notre cas.
results.sort_values(by='Coherence', ascending=False)
D'après nos résultats, le nombre de topic placé à deux est le meilleur et on remarque que alpha et beta assez haut sont des bons paramètres. Ce qui veut dire que nos topics et nos mots sont assez denses.
results.groupby(by='Topics').Coherence.mean().plot(title='Average Coherence for number of Topics (Unigram on TAK column filtered with lang="fr")')
pass
On peut alors clairement voir que la meilleure cohérence est donnée par 2 topics. Avant de faire cette étude sur 2 topics nous allons tout de même essayer avec 6 pour comparer avec notre KMeans fait précédemment.
(Nous aurions pu aller vers les 10 topics puisque la coherence semble augmenter avec le nombre de topics à partir de 7 mais cela serait trop de topics pour une analyse simple)
%%capture
num_topics = 6
alpha = 0.61
beta = 0.61
lda_model = gensim.models.LdaMulticore(
corpus=corpus_bow,
id2word=corpus_dict,
num_topics=num_topics,
random_state=42,
passes=20,
alpha=alpha,
eta=beta
)
viz = pyLDAvis.gensim.prepare(lda_model, corpus_bow, corpus_dict)
# save HTML pag of visualization
vis_filename = f'LDA_FR_TAK_UNIGRAM_{num_topics}TOPICS_{alpha}ALPHA_{beta}BETA.html'
pyLDAvis.save_html(viz, os.path.join(save_dir, vis_filename))
viz
La visualisation est disponible est exportée aussi sous format LDA_{lang}_{data}_{ngram}_{numtopics}TOPICS\{alpha}ALPHA_{beta}BETA.html
En placant le lambda = 0.2, on peut alors se rendre compte de clusters assez semblables à ce que l'on avait avec KMeans. On retrouve :
%%capture
num_topics = 2
alpha = 0.61
beta = 0.61
lda_model = gensim.models.LdaMulticore(
corpus=corpus_bow,
id2word=corpus_dict,
num_topics=num_topics,
random_state=42,
passes=20,
alpha=alpha,
eta=beta
)
viz = pyLDAvis.gensim.prepare(lda_model, corpus_bow, corpus_dict)
# save HTML pag of visualization
vis_filename = f'LDA_FR_TAK_UNIGRAM_{num_topics}TOPICS_{alpha}ALPHA_{beta}BETA.html'
pyLDAvis.save_html(viz, os.path.join(save_dir, vis_filename))
viz
La visualisation est disponible est exportée aussi sous format LDA_{lang}_{data}_{ngram}_{numtopics}TOPICS\{alpha}ALPHA_{beta}BETA.html
En placant le lambda = 0.2, on peut alors se rendre compte de clusters bien définis. On retrouve :
data = list(bigram_corpus_fr[bigram_corpus_fr.lang == 'fr'].tak)
# get the dictionary of the corpus as token ids
corpus_dict = gensim.corpora.Dictionary(data)
# get the bag-of-words (as tuple (token_id, token_count))
corpus_bow = [corpus_dict.doc2bow(word) for word in data]
Dans un soucis de coherence nous allons garder nos hyperparamètres :
%%capture
num_topics = 2
alpha = 0.61
beta = 0.61
lda_model = gensim.models.LdaMulticore(
corpus=corpus_bow,
id2word=corpus_dict,
num_topics=num_topics,
random_state=42,
passes=20,
alpha=alpha,
eta=beta
)
viz = pyLDAvis.gensim.prepare(lda_model, corpus_bow, corpus_dict)
# save HTML pag of visualization
vis_filename = f'LDA_FR_TAK_BIGRAM_{num_topics}TOPICS_{alpha}ALPHA_{beta}BETA.html'
pyLDAvis.save_html(viz, os.path.join(save_dir, vis_filename))
viz
La visualisation est disponible est exportée aussi sous format LDA_{lang}_{data}_{ngram}_{numtopics}TOPICS\{alpha}ALPHA_{beta}BETA.html
En placant le lambda = 0.2, on peut alors se rendre compte de clusters bien définis mais totalement différents de ceux en unigram. On retrouve :
data = list(unigram_corpus_en[unigram_corpus_en.lang == 'en'].tak)
# get the dictionary of the corpus as token ids
corpus_dict = gensim.corpora.Dictionary(data)
# get the bag-of-words (as tuple (token_id, token_count))
corpus_bow = [corpus_dict.doc2bow(word) for word in data]
Dans un soucis de coherence nous allons garder nos hyperparamètres :
%%capture
num_topics = 2
alpha = 0.61
beta = 0.61
lda_model = gensim.models.LdaMulticore(
corpus=corpus_bow,
id2word=corpus_dict,
num_topics=num_topics,
random_state=42,
passes=20,
alpha=alpha,
eta=beta
)
viz = pyLDAvis.gensim.prepare(lda_model, corpus_bow, corpus_dict)
# save HTML pag of visualization
vis_filename = f'LDA_EN_TAK_UNIGRAM_{num_topics}TOPICS_{alpha}ALPHA_{beta}BETA.html'
pyLDAvis.save_html(viz, os.path.join(save_dir, vis_filename))
viz
La visualisation est disponible est exportée aussi sous format LDA_{lang}_{data}_{ngram}_{numtopics}TOPICS\{alpha}ALPHA_{beta}BETA.html
En se placant toujours sur lambda = 0.2, nous avons plus de mal pour les unigram anglais à définir les deux clusters. Mais si on devait résumer tout de mêême on pourrait dire :
Cependant il est à prendre à la légère car nous sommes avec un petit dataset de seulement 100 données alors il est difficile à analyser comparé à sa complexité dans son vocabulaire.
Et enfin pour terminer nous allons revenir sur le contenu en lui même. Nous ne le ferons pas en bigram ni en anglais toujours pour la raison de complexité de vocabulaire.
data = list(unigram_corpus_fr[unigram_corpus_fr.lang == 'fr'].content)
# get the dictionary of the corpus as token ids
corpus_dict = gensim.corpora.Dictionary(data)
# get the bag-of-words (as tuple (token_id, token_count))
corpus_bow = [corpus_dict.doc2bow(word) for word in data]
Oublions le soucis de cohérence et revenons à 6 topics :
%%capture
num_topics = 6
alpha = 0.61
beta = 0.61
lda_model = gensim.models.LdaMulticore(
corpus=corpus_bow,
id2word=corpus_dict,
num_topics=num_topics,
random_state=42,
passes=10,
alpha=alpha,
eta=beta
)
viz = pyLDAvis.gensim.prepare(lda_model, corpus_bow, corpus_dict)
# save HTML pag of visualization
vis_filename = f'LDA_FR_CONTENT_UNIGRAM_{num_topics}TOPICS_{alpha}ALPHA_{beta}BETA.html'
pyLDAvis.save_html(viz, os.path.join(save_dir, vis_filename))
viz
La visualisation est disponible est exportée aussi sous format LDA_{lang}_{data}_{ngram}_{numtopics}TOPICS\{alpha}ALPHA_{beta}BETA.html
Avant même de placer notre lambda, on constate direcement que le nombre de topic aurait dû être placé à 2 car les quatres derniers ne sont que des cas très particuliers et la majorité se retrouve dans les deux premier clusters.
En placant le lambda = 0.2, on peut alors faire le lien avec ce que l'on a trouvé avec nos bigram sur le titre / abstract / keyword. On retrouve :
# Save HTML and PDF and MD
!jupyter nbconvert --to html "/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/project_1.ipynb" --output-dir="/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/save_dir"
!jupyter nbconvert --to pdf "/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/project_1.ipynb" --output-dir="/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/save_dir"
!jupyter nbconvert --to markdown "/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/project_1.ipynb" --output-dir="/content/drive/MyDrive/A5/Advanced Machine Learning for Big Data and Text Processing/project1/save_dir"